#include "network/tcp_client.hpp"

#include "base/assert.hpp"
#include "base/log.hpp"
#include "network/tcp_connection.hpp"
#include "network/tcp_connector.hpp"

#include <memory>

namespace ibox::network {

struct TcpClient::Data {
    event::Loop *m_loop;
    State state = State::None;

    ConnectedCallback connected_cb;
    DisconnectedCallback disconnected_cb;
    ReceiveCallback received_cb;
    size_t received_threshold = 0;
    ByteStream *m_receiver = nullptr;
    bool reconnect_enabled = true;

    std::unique_ptr<TcpConnector> m_connector{};
    TcpConnection *m_connection = nullptr;
};

TcpClient::TcpClient(event::Loop *loop) : m_data{std::make_unique<Data>()} {
    expect(m_data != nullptr);

    m_data->m_loop = loop;
    m_data->m_connector = std::make_unique<TcpConnector>(loop);
}

TcpClient::~TcpClient() {

    cleanup();

    delete m_data->m_connection;
}

bool TcpClient::initialize(const SockAddr &server_addr) {
    if (m_data->state != State::None) {
        LOG_WARNING("not in none state, cleanup first.");
        return false;
    }

    using namespace std::placeholders;
    m_data->m_connector->initialize(server_addr);
    m_data->m_connector->set_try_times(8);
    m_data->m_connector->set_connected_callback(
        [&](TcpConnection *new_conn) { on_tcp_connected(new_conn); });

    m_data->state = State::Inited;
    return true;
}

void TcpClient::set_connected_callback(const ConnectedCallback &cb) {
    m_data->connected_cb = cb;
}

void TcpClient::set_disconnected_callback(const DisconnectedCallback &cb) {
    m_data->disconnected_cb = cb;
}

void TcpClient::set_auto_reconnect(bool enable) {
    m_data->reconnect_enabled = enable;
}

bool TcpClient::start() {
    if (m_data->state != State::Inited) {
        LOG_WARNING("not in idle state, initialize or stop first");
        return false;
    }

    m_data->state = State::Connecting;
    return m_data->m_connector->start();
}

void TcpClient::stop() {
    if (m_data->state == State::Connecting) {
        m_data->state = State::Inited;
        m_data->m_connector->stop();

    } else if (m_data->state == State::Connected) {
        m_data->m_connection->disconnect();
        delete m_data->m_connection;

        m_data->state = State::Inited;
    }
}

bool TcpClient::shutdown(int howto) {
    if (m_data->state == State::Connected) {
        return m_data->m_connection->shutdown(howto);
    }
    return false;
}

void TcpClient::cleanup() {
    if (m_data->state <= State::None) {
        return;
    }

    stop();

    m_data->m_connector->cleanup();

    m_data->connected_cb = nullptr;
    m_data->disconnected_cb = nullptr;
    m_data->received_cb = nullptr;
    m_data->received_threshold = 0;
    m_data->m_receiver = nullptr;
    m_data->reconnect_enabled = true;

    m_data->state = State::None;
}

TcpClient::State TcpClient::state() const { return m_data->state; }

void TcpClient::set_receive_callback(ReceiveCallback cb, size_t threshold) {
    if (m_data->m_connection != nullptr) {
        m_data->m_connection->set_receive_callback(cb, threshold);
    }

    m_data->received_cb = cb;
    m_data->received_threshold = threshold;
}

bool TcpClient::send(const void *data_ptr, size_t data_size) {
    if (m_data->m_connection != nullptr) {
        return m_data->m_connection->send(data_ptr, data_size);
    }

    return false;
}

void TcpClient::bind(ByteStream *receiver) {
    if (m_data->m_connection != nullptr) {
        m_data->m_connection->bind(receiver);
    }

    m_data->m_receiver = receiver;
}

void TcpClient::unbind() {
    if (m_data->m_connection != nullptr) {
        m_data->m_connection->unbind();
    }

    m_data->m_receiver = nullptr;
}

void TcpClient::on_tcp_connected(TcpConnection *new_conn) {
    new_conn->set_disconnected_callback([&]() { on_tcp_disconnected(); });
    new_conn->set_receive_callback(m_data->received_cb,
                                   m_data->received_threshold);
    if (m_data->m_receiver != nullptr) {
        new_conn->bind(m_data->m_receiver);
    }

    m_data->m_connection = new_conn;

    m_data->state = State::Connected;

    if (m_data->connected_cb) {
        m_data->connected_cb();
    }
}

void TcpClient::on_tcp_disconnected() {
    delete m_data->m_connection;
    m_data->m_connection = nullptr;

    m_data->state = State::Inited;

    if (m_data->reconnect_enabled) {
        start();
    }

    if (m_data->disconnected_cb) {
        m_data->disconnected_cb();
    }
}

} // namespace ibox::network
